/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { ArkAssignStmt, ArkClass, ArkMethod, Stmt, ts, NumberType, ArkIfStmt, ArkInvokeStmt, ArkPtrInvokeExpr, FunctionType, ArkInstanceInvokeExpr } from "arkanalyzer";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseMetaData, BaseChecker } from "../BaseChecker";
import { Rule, Defects, ClassMatcher, MatcherTypes, MatcherCallback,MethodMatcher} from '../../Index';
import { RuleListUtil} from "../../utils/common/DefectsList";
import { Local } from "arkanalyzer/lib/core/base/Local";
import { IssueReport } from "../../model/Defects";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoLoopFuncCheck');
const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/no-loop-func-check.md", // TODO: support url
    description: "Function declared in a loop contains unsafe references to variable(s) "
};

export class NoLoopFuncCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    public leftOps: any[] = [];
    public isForleftOps: any[] = [];
    private clsMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS
    };
    private buildMatcher: MethodMatcher = {
        class: [this.clsMatcher],
        matcherType: MatcherTypes.METHOD,
    };
    public registerMatchers(): MatcherCallback[] {
        const matchBuildCb: MatcherCallback = {
            matcher: this.buildMatcher,
            callback: this.check
        };
        return [matchBuildCb];
    }
    public check = (targetMtd: ArkMethod) => {
        const originPath = targetMtd.getDeclaringArkClass().getDeclaringArkFile().getName();
        if (!this.isTsFile(originPath)) {
            return;
        }
        const severity = this.rule.alert ?? this.metaData.severity;
        const stmts = targetMtd.getBody()?.getCfg().getStmts() ?? [];
        for (const stmt of stmts) {
            const originalText = stmt?.getOriginalText() ?? '';
            if (!originalText) continue;
            if (stmt instanceof ArkAssignStmt) {
                this.isForleftOps = [];
                const leftop = stmt.getLeftOp()
                if (leftop instanceof Local) {
                    const letOrVarRegex = /\b(let|var)\b/;
                    const hasLetOrVar = letOrVarRegex.test(originalText);
                    const leftopName = leftop.getName();
                    if (hasLetOrVar) {
                        if (!originalText.startsWith('for')) {
                            this.leftOps.push(leftopName);
                        } else if (originalText.startsWith('for')) {
                            const letRegex = /\blet\s+\w+\s*;\s*/g;
                            const matches = originalText.match(letRegex);
                            if (matches && matches.length > 0) {
                                this.isForleftOps.push(matches[0]);
                            }
                            const varRegex = /\bvar\b/;
                            const hasVar = varRegex.test(originalText);
                            if (hasVar) {
                                this.isForleftOps.push(leftopName);
                            }
                        }
                    }
                }
                const sourceFile = ts.createSourceFile("temp.ts", originalText, ts.ScriptTarget.Latest, true);
                ts.forEachChild(sourceFile, (node) => {
                    if (ts.isForInStatement(node) || ts.isForOfStatement(node) || ts.isForStatement(node)) {
                        this.checkLoopBody(node, severity, stmt);
                    }
                });
            } else if (stmt instanceof ArkIfStmt) {
                this.isForleftOps = [];
                const leftop = stmt.getConditionExpr().getOp1();
                const sourceFile = ts.createSourceFile("temp.ts", originalText, ts.ScriptTarget.Latest, true);
                ts.forEachChild(sourceFile, (node) => {
                    if (ts.isWhileStatement(node) || ts.isDoStatement(node)) {
                        this.checkLoopBody(node, severity, stmt);
                    }
                });
            }
        }
    }

    private checkLoopBody(node: ts.Node, severity: number, stmt: Stmt) {
        this.checkFunctionExpression(node, severity, stmt);
    }
    private checkFunctionExpression(node: ts.Node, severity: number, stmt: Stmt) {
        if (ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isFunctionDeclaration(node)) {
            const functionText = node.getText();
            const isForleftOps = this.referencesLeftOps(node, this.leftOps)
            const isForleftOps2 = this.referencesLeftOps(node, this.isForleftOps)

            if (isForleftOps != '' || isForleftOps2 != '') {
                let message = ''
                if (isForleftOps != '') {
                    message = this.metaData.description + isForleftOps
                } else {
                    message = this.metaData.description + isForleftOps2
                }
                this.addIssueReport(stmt, functionText, message, severity);
            }
        } else {
            ts.forEachChild(node, (childNode) => {
                this.checkFunctionExpression(childNode, severity, stmt);
            });
        }
    }
    private referencesLeftOps(funcNode: ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction, leftOps: string[]): string {
        let isForleftOps = '';
        const checkNode = (node: ts.Node) => {
            if (ts.isIdentifier(node) && leftOps.includes(node.getText())) {
                isForleftOps = node.getText();
                return;
            }
            ts.forEachChild(node, checkNode);
        };
        if (ts.isFunctionDeclaration(funcNode) || ts.isFunctionExpression(funcNode)) {
            const body = funcNode.body;
            if (body) {
                ts.forEachChild(body, checkNode);
            }
        } else if (ts.isArrowFunction(funcNode)) {
            const body = funcNode.body;
            if (ts.isBlock(body)) {
                ts.forEachChild(body, checkNode);
            } else {
                checkNode(body);
            }
        }
        return isForleftOps;
    }
    private addIssueReport(stmt: Stmt, name: string, message: string, severity: number) {
        const warnInfo = this.getLineAndColumn(stmt, name);
        const fixKey = `${warnInfo.line}%${warnInfo.startCol}%${warnInfo.endCol}%${this.rule.ruleId}`;
        const mergeKey = `${warnInfo.filePath}%${fixKey}%${message}`;
        const defects = (new Defects(warnInfo.line, warnInfo.startCol, message, severity, this.rule.ruleId,
            mergeKey, this.metaData.ruleDocPath, true, false, false, fixKey));
        this.issues.push(new IssueReport(defects, undefined));
        RuleListUtil.push(defects);
    }

    private getLineAndColumn(stmt: Stmt, name: string) {
        const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
        if (arkFile) {
            const originText = stmt.getOriginalText() ?? '';
            const pos = originText.indexOf(name);
            if (pos !== -1) {
                const returnIndex = originText.indexOf(name);
                const substring = originText.substring(0, returnIndex);
                const newlineCount = substring.split("\r\n").length - 1;
                const originPosition = stmt.getOriginPositionInfo();
                const line = originPosition.getLineNo() + newlineCount;
                let startCol = originPosition.getColNo();
                const lastNewLineIndex = originText.lastIndexOf('\r\n', returnIndex - 1);
                if (lastNewLineIndex !== -1) {
                    startCol = returnIndex - lastNewLineIndex - 2;
                }
                const endCol = startCol + name.length - 1;
                const originPath = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile().getFilePath();
                return { line, startCol, endCol, filePath: originPath };
            }
        }
        return { line: -1, startCol: -1, endCol: -1, filePath: '' };
    }
    private isTsFile(filePath: string): boolean {
        return filePath.toLowerCase().endsWith('.ts');
    }
}